mmap — 파일과 공유메모리를 포인터로 붙이기

NOTE

mmap은 파일이나 메모리 덩어리를 내 프로세스의 가상 주소 공간에 “붙여주는” 시스템 콜이다. 붙이고 나면 read/write 없이 포인터로 바로 읽고 쓴다. POSIX 공유메모리는 shm_open으로 fd를 얻고 이 mmap으로 붙이는 방식이다.

상위 허브: 공유메모리 한눈에 보기 · 전제 지식: 프로세스 가상 주소 공간


1. mmap이 하는 일 한 장 그림

        [ 대상 ]                         [ 내 프로세스의 가상 주소 공간 ]
  디스크 파일 / 공유메모리 / 빈 메모리            ┌──────────────┐
          │                                      │ ...          │
          │   mmap(fd, 길이, 권한, 플래그)         │ ▒ 붙인 구간 ▒│ ← 반환된 포인터 p
          └────────────────────────────────────→ │ ...          │
                                                 └──────────────┘
   p[0] = 'A';   ← 포인터에 쓰면 곧 대상(메모리/파일)에 쓰는 것. read/write 호출 없음

C/Java 비교:

  • C malloc: “내 프로세스 전용 heap에서 빌리기”
  • C mmap: “커널이 관리하는 어떤 대상(파일·공유메모리·빈 메모리)을 내 주소 공간에 연결하기”
  • Java: FileChannel.map(...)이 돌려주는 MappedByteBuffer가 바로 이 mmap을 감싼 것이다.

2. 함수 시그니처 해독

#include <sys/mman.h>
 
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int   munmap(void *addr, size_t length);
인자의미자주 쓰는 값
addr붙일 위치 희망값NULL (커널이 알아서 고르게 함)
length붙일 길이(바이트)공유메모리 크기 (내부적으로 페이지 단위 올림)
prot접근 권한PROT_READ, PROT_WRITE, PROT_NONE
flags공유 방식MAP_SHARED(다른 프로세스와 공유), MAP_PRIVATE, MAP_ANONYMOUS
fd붙일 대상의 파일 디스크립터shm_open/open이 준 fd (MAP_ANONYMOUS면 -1)
offset대상의 시작 위치보통 0 (페이지 크기의 배수여야 함)
반환값붙은 구간의 시작 포인터실패 시 NULL이 아니라 MAP_FAILED

3. 무엇을 붙이느냐에 따른 종류

종류붙이는 대상용도
파일 매핑 + MAP_SHARED디스크 파일큰 파일을 read 없이 접근, 수정이 파일에 반영됨
파일 매핑 + MAP_PRIVATE디스크 파일내 복사본만 수정(copy-on-write). 실행 파일·공유 라이브러리 로딩이 이 방식
익명 매핑 (MAP_ANONYMOUS)파일 없는 빈 메모리malloc이 큰 크기를 요청받을 때 내부 사용
/dev/shm 파일 + MAP_SHAREDshm_open이 준 fdPOSIX 공유메모리의 실체

System V 공유메모리의 shmat은 이 mmap에 해당하는 단계다(API만 다른 별도 방식). → 세그먼트 노트


4. 붙이는 순간 복사하지 않는다

mmap 호출 때는 가상 주소 구간만 예약되고, 데이터는 처음 접근할 때 페이지 폴트로 물리 메모리에 올라온다(Demand Paging, → 가상 주소 공간). 그래서 1GB 파일도 mmap 호출 자체는 즉시 끝난다. 같은 MAP_SHARED 대상을 여러 프로세스가 붙이면 각자 다른 가상 주소가 같은 물리 페이지를 가리킨다.

프로세스 A 가상: 0x7f3a1000 ──┐
                              ├──→ 물리 페이지 1개 (RAM)
프로세스 B 가상: 0x7e90c000 ──┘
(가상 주소는 달라도 같은 물리 메모리)

5. 붙이고 나면 포인터로 그냥 쓴다

char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) { /* 실패 처리 */ }
 
p[0] = 'A';                        /* 배열처럼 */
strcpy(p + 10, "hello");           /* 문자열 함수도 가능 */
 
struct Msg *m = (struct Msg *)p;   /* 구조체로 캐스팅해서 */
m->count++;                        /* 필드 접근도 가능 */
 
munmap(p, 4096);                   /* 다 쓰면 떼어냄 (free 아님!) */

malloc으로 받은 포인터와 사용법이 같다. 다른 점은 “어디에 있는 메모리인가”와 “해제 방법”이다.

완전한 POSIX 공유메모리 예 (생성 측)

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
 
struct Shared { int count; char name[32]; };
 
int fd = shm_open("/myshm", O_CREAT | O_EXCL | O_RDWR, 0660);  /* 이미 있으면 실패(EEXIST) */
ftruncate(fd, sizeof(struct Shared));                           /* 크기 지정 (한 번) */
struct Shared *s = mmap(NULL, sizeof(*s), PROT_READ | PROT_WRITE,
                        MAP_SHARED, fd, 0);
close(fd);                      /* fd를 닫아도 매핑은 유지됨 */
/* ... 사용 ... */
munmap(s, sizeof(*s));
shm_unlink("/myshm");           /* 생성자가 마지막에 삭제 */

사용 측은 O_CREAT 없이 shm_open("/myshm", O_RDWR, 0) 후 mmap만 하고, 끝나면 munmap만 한다. (일부 구형 glibc에서는 링크 시 -lrt 필요)


6. 주의점

주의점설명
포인터는 그 프로세스에서만 유효같은 세그먼트를 다른 프로세스가 붙이면 다른 주소를 받는다(가상 주소 + ASLR). 포인터 값을 넘겨도 쓸 수 없다. 공유메모리 안에는 포인터 대신 오프셋(시작점 기준 상대 위치) 을 저장한다.
PROT_* 위반PROT_READ만 줬는데 쓰면 SIGSEGV
범위 초과length 밖을 건드리면 SIGSEGV거나 옆 메모리 오염 (버퍼 오버플로우와 동일)
해제는 munmapfree(p)를 부르면 크래시. 짝은 munmap
동기화는 직접여러 프로세스가 같은 곳을 쓰면 락이 필요 → 공유메모리 동시성
구조체 캐스팅은 레이아웃 일치가 전제컴파일 옵션·아키텍처가 다르면 패딩이 달라 필드가 어긋남 → 구조체 패딩
정렬mmap 반환 주소는 항상 페이지 경계(4KB) 에서 시작하므로 어떤 타입의 정렬 요구도 시작 주소에서는 만족한다. 구조체 내부 패딩만 신경 쓰면 된다.

7. 한눈에 정리

질문답
mmap이 뭔가파일/공유메모리/빈 메모리를 내 가상 주소 공간에 붙여 포인터로 접근하게 하는 시스템 콜
붙이고 바로 포인터로 쓰나그렇다. malloc 포인터처럼 쓴다 (단, 그 프로세스 안에서만 유효)
데이터를 복사하나붙일 때는 복사 없음. 접근한 페이지만 물리 메모리에 올라옴
공유메모리와의 관계POSIX: shm_open + mmap / System V: shmget + shmat
해제munmap (free 아님). 세그먼트 삭제는 shm_unlink(별도 단계)
실패 반환값MAP_FAILED (NULL 아님)

관련 문서